package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import com.hmdp.utils.SystemConstants;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 查询商铺缓存：
 *      -- 这里面采用 Redis 中的字符串来存储缓存 不用 hash
 * 缓存更新策略：
 *      -- 内存淘汰 缓存数据达到一定数量后，淘汰一部分数据
 *      -- 超时剔除 缓存数据超时后就删除
 *      -- 主动更新 用户自己编码解决数据一致
 *          -- 用户自己更新
 *          -- 写穿透 每次写数据的时候同时写到数据库和缓存中  代价就是 降低了性能
 *          -- 回写法：每次只操作缓存 缓存每隔一段时间自己把数据批量同步到数据库
 *      -- 其他问题：
 *          -- 删除缓存还是更新缓存：
 *              -- 删除 等下次查询的时候会自动把最新的数据重新写进缓存
 *              -- 更新的话如果我只有写操作没有查询 那更新就没意义了 浪费性能
 *          -- 添加事务注解
 *          -- 先操作缓存还是先操作数据 ???
 *              -- 先删除缓存 此时准备写数据库 但此时有个线程把数据库的旧数据读出来写入缓存 线程 1 又接着执行更新数据库操作 于是就不一致了
 *              -- 先删除数据库 比如线程 1 操作的时候缓存恰好失效 去查数据库 于是他就拿到了旧数据 于是线程 2 删除完数据库和缓存后 线程 1 再去把旧数据写进缓存
 *      -- 最佳实践：
 *              --先操作数据库 再删除缓存
 * 缓存常见问题：
 *      -- 缓存穿透：正常情况下如果数据在数据库里存在 那么只有第一次请求需要访问数据库 后续的请求可以直接在 Redis 中找到
 *          但是如果数据本身不存在 那就不会同步到 Redis 中 于是每次访问都是 Redis 获取为空 然后去访问数据库
 *      -- 于是如果一直访问不存在的数据 这样数据库就会一致被访问到 跟裸奔一样
 *      -- 解决方案：
 *          -- 缓存空对象：
 *              -- 当查询缓存失败 接着查询数据库的时候 如果没查到不是直接抛异常 而是在缓存中写入一个 空对象 ""
 *                  然后查询的时候先判断下命中的是不是 "" 如果是 "" 说明不存在这个 直接返回空 于是就不会打到数据库了
 *                  这个数据存在不存在最后都只请求一次数据库
 *              -- 缺点就是有垃圾存在 Redis 中 当然可以设置过期时间 也可以新增数据的时候主动将数据插入缓存 覆盖空数据
 *                  会造成短期不一致 可能正好这个时候真的插入了这个数据
 *          -- 布隆过滤器：
 *              -- 这个就是在客户端和缓存中添加了一个中间层 客户端先去询问布隆过滤器这个数据是否存在 不存在的话 (数据库不存在)
 *                  就直接返回 存在的话(可能缓存中存在也可能不存在)
 *              -- 原理：难道布隆过滤器把所有数据存了一遍 ? 不是 那样有数据冗余
 *                  -- 布隆过滤器就是一个哈希表
 *                      你在插入数据库数据的时候 把数据计算出一个哈希值 然后把对应哈希值的地方改成 true
 *                      你在判断某个数据存在的时候也是算出他的哈希值 然后判断对应哈希值是否为 true
 *                      这种直接基于索引的访问比扫描一个个找快
 *                      当然由于哈希冲突的存在 可能他告诉你存在的时候其实是另一条哈希值一样的数据存在 而不是当前这个数据存在
 *                      说不存在的时候就一定不存在你
 *                  -- 由于这种不准确 可能有个数据他告诉我存在 但是不存在实际上 我仍然去访问数据库 缓存 于是又缓存穿透了
 *              -- 优点：占用内存少
 *              -- 缺点：误判和复杂度高
 *      -- 缓存雪崩：
 *          -- 同一时间有大量的 key 过期或者 Redis 直接崩溃了 跟缓存击穿的区别是 缓存击穿中过期的 key 是热点 key 而且是一部分 key
 *          -- 造成的后果就是一瞬间会有大量的请求直接到达数据库 可能导致数据库崩溃
 *          -- 解决方案：
 *              -- 给 key 的过期事件设置成随机值 因为有时候把数据批量导入缓存 如果设置成一样的 就可能一起过期
 *              -- Redis 集群 这样避免 Redis 直接挂机
 *              -- 给服务添加熔断降级 就是发现 Redis 挂机后尽快去限制请求的数量 防止数据库被压垮
 *              -- 添加多级缓存
 *      -- 缓存击穿问题：
 *          -- 也称为 热点 key 过期问题 就是一个被高并发访问的热点 key 过期 而且这个数据重建时间长 (比如查完数据后还要做一些处理)
 *          -- 怎么理解 ：?
 *              -- 比如缓存空了 key 过期 于是线程 1 去查询数据库 构建缓存 这期间缓存里是没有这个数据的 那么其他的线程访问的时候都会
 *                  查到空 于是都跑去访问数据库 都去重建缓存 于是数据库压力就起来了
 *          -- 解决方案：
 *              -- 互斥锁：
 *                  -- 本质是一大堆的线程来重建 提高了数据库压力 那我第一个线程来重建的时候我加一个锁 保证只有一个线程在重建
 *                      于是就减轻了数据库压力 当然其他的线程获取锁失败后要重试 而且最好是延迟后再重试 重试是重新查询缓存 于是就是调用自己一次
 *                  -- 缺点就是：如果一大堆的线程来了 只有一个在重建 其他的只能等
 *              -- 逻辑过期：
 *                  -- 缓存失效就是因为设置了 ttl 于是现在不设置 ttl 而是在缓存的对象中设置一个过期时间字段
 *                      下次查的时候再判断下过没过期 如果过期了就重新重建缓存 当然这里的重建不是线程自己重建 因为那样还是阻塞式
 *                      我们会开启一个新线程来重建 而且整个过程也是加锁 其他的线程此时来就直接响应旧数据 线程1让别人重建缓存 自己也不会去等 而是直接返回旧数据
 *              -- 怎么自定义一个互斥锁 ???
 *                  -- 用 setnx 命令 只有第一个操作的会成功 其他的再来会失败
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    private final StringRedisTemplate stringRedisTemplate ;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10) ;

    /**
     * 根据id查询商铺信息
     *
     * @param id
     * @return
     */
    @Override
    public Shop queryById(Long id) throws InterruptedException {
        // 1.查询缓存中的商品数据
        String key = RedisConstants.CACHE_SHOP_KEY + id ;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在 直接返回 只有数据是字符串的时候才为 true
            log.info("缓存命中！");
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop ;
        }
        // 判断下是不是 ""
        if (shopJson != null) {
            log.info("缓存命中 但是是空数据 返回错误信息");
            throw new RuntimeException("商品不存在！") ;
        }
        log.info("缓存未命中！");
        String lockKey;    // 每个店铺一把锁
        Shop shop;
        lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        try {
            // 实现缓存重建
            // 获取互斥锁
            boolean isLock = tryLock(lockKey);
            // 判断是否获取成功
            if (!isLock) {
                // 失败 休眠并重试
                Thread.sleep(RedisConstants.LOCK_SHOP_RETRY_TTL);
                // 递归调用自己
                return queryById(id) ;
            }
            // 成功 执行缓存重建

            // 4.不存在 根据 id 查询数据库
            shop = this.getById(id);
            if (shop == null) {
                // 5.不存在 返回 404
                // 将 "" 空数据写入缓存
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                throw new RuntimeException("商品不存在！") ;
            }
            // 6.存在 写入缓存
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } finally {
            // 释放互斥锁
            unlock(lockKey);
        }
        // 7.返回
        return shop ;
    }


    /**
     * 执行缓存重建
     */
    public void saveShop2Redis(Long id , Long expireSeconds) {
        log.info("开始执行缓存重建！");
        // 查询店铺信息
        Shop shop = this.getById(id);
        // 封装逻辑过期时间
        RedisData redisData = RedisData.builder()
                .data(shop)
                .expireTime(LocalDateTime.now().plusSeconds(expireSeconds))
                .build();
        // 写入 Redis
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 更新商铺信息
     *
     * @param shop
     */
    @Override
    @Transactional
    public void update(Shop shop) {
        // 1.判空
        Long id = shop.getId() ;
        if (id == null) {
            throw new RuntimeException("店铺不存在！") ;
        }
        // 1.更新数据库
        this.updateById(shop);
        // 2.删除缓存
        stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);
    }

    /**
     * 根据类型查询商铺信息
     *
     * @param typeId
     * @param current
     * @param x
     * @param y
     * @return
     */
    @Override
    public List<Shop> queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        // 判断是否需要根据坐标查询
        if (x == null || y == null) {
            // 不需要坐标查询，直接根据类型分页查询
            Page<Shop> page = this.query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            // 返回
            return page.getRecords() ;
        }
        // 计算分页参数
        int from = (current - 1 ) * SystemConstants.DEFAULT_PAGE_SIZE ;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE ;
        // 查询 redis 按照距离排序 分页 结果：shopId , distance
        String key = RedisConstants.SHOP_GEO_KEY + typeId ;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
                .search(key,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(RedisConstants.SHOP_GEO_RADIUS),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));// 返回 0 ~ end

        // 解析出 id
        if (results == null) {
            return Collections.emptyList() ;
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        if (list.size() <= from) {
            // 没有下一页了
            return Collections.emptyList() ;
        }
        // 截取 from ~ end 列表
        List<Long> ids = new ArrayList<>() ;
        Map<String,Distance> distanceMap = new HashMap<>(list.size()) ;
        list.stream().skip(from).forEach(result -> {
            // 这里可能把数据都跳过去了
            // 所以如果 list.size() < from 就可以直接返回了
            // 获取店铺 id
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr)) ;
            // 获取距离
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr,distance) ;
        });
        // 查询 Shop  保证有序
        String idStr = StrUtil.join(",", ids);
        List<Shop> shopList = this.query()
                .in("id", ids)
                .last("ORDER BY FIELD(id," + idStr + ")").list();
        shopList.forEach(shop -> {
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue()) ;
        });
        // 返回
        return shopList ;
    }

    /**
     * 尝试获取锁
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag) ;
    }

    /**
     * 释放锁
     * @param key
     */
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

    /**
     * 根据id查询商铺信息
     *
     * @param id
     * @return
     */
    public Shop queryById_01(Long id) {
        // 1.查询缓存中的商品数据
        String key = RedisConstants.CACHE_SHOP_KEY + id ;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在 直接返回
            log.info("缓存命中！");
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop ;
        }
        log.info("缓存未命中！");
        // 4.不存在 根据 id 查询数据库
        Shop shop = this.getById(id);
        if (shop == null) {
            // 5.不存在 返回 404
            throw new RuntimeException("商品不存在！") ;
        }
        // 6.存在 写入缓存
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        // 7.返回
        return shop ;
    }

    /**
     * 根据id查询商铺信息   缓存穿透
     *
     * @param id
     * @return
     */
    public Shop queryByIdWithCacheThrough(Long id) {
        // 1.查询缓存中的商品数据
        String key = RedisConstants.CACHE_SHOP_KEY + id ;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在 直接返回 只有数据是字符串的时候才为 true
            log.info("缓存命中！");
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop ;
        }
        // 判断下是不是 ""
        if (shopJson != null) {
            log.info("缓存命中 但是是空数据 返回错误信息");
            throw new RuntimeException("商品不存在！") ;
        }
        log.info("缓存未命中！");
        // 4.不存在 根据 id 查询数据库
        Shop shop = this.getById(id);
        if (shop == null) {
            // 5.不存在 返回 404
            // 将 "" 空数据写入缓存
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            throw new RuntimeException("商品不存在！") ;
        }
        // 6.存在 写入缓存
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        // 7.返回
        return shop ;
    }

    /**
     * 根据id查询商铺信息   互斥锁
     *
     * @param id
     * @return
     */
    public Shop queryByIdWithMutex(Long id) throws InterruptedException {
        // 1.查询缓存中的商品数据
        String key = RedisConstants.CACHE_SHOP_KEY + id ;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在 直接返回 只有数据是字符串的时候才为 true
            log.info("缓存命中！");
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop ;
        }
        // 判断下是不是 ""
        if (shopJson != null) {
            log.info("缓存命中 但是是空数据 返回错误信息");
            throw new RuntimeException("商品不存在！") ;
        }
        log.info("缓存未命中！");
        String lockKey;    // 每个店铺一把锁
        Shop shop;
        lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        try {
            // 实现缓存重建
            // 获取互斥锁
            boolean isLock = tryLock(lockKey);
            // 判断是否获取成功
            if (!isLock) {
                // 失败 休眠并重试
                Thread.sleep(RedisConstants.LOCK_SHOP_RETRY_TTL);
                // 递归调用自己
                return queryById(id) ;
            }
            // 成功 执行缓存重建

            // 4.不存在 根据 id 查询数据库
            shop = this.getById(id);
            if (shop == null) {
                // 5.不存在 返回 404
                // 将 "" 空数据写入缓存
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                throw new RuntimeException("商品不存在！") ;
            }
            // 6.存在 写入缓存
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } finally {
            // 释放互斥锁
            unlock(lockKey);
        }
        // 7.返回
        return shop ;
    }

    /**
     * 根据id查询商铺信息   逻辑过期 因为这里的数据事先写入缓存 不会不命中 就直接返回 null 也不请求数据库 所以没有缓存穿透问题
     *      -- 这个要事先将数据导入缓存 因此不适合当前业务
     *
     * @param id
     * @return
     */
    public Shop queryByIdWithLogicalExpire(Long id) throws InterruptedException {
        // 1.查询缓存中的商品数据
        String key = RedisConstants.CACHE_SHOP_KEY + id ;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            // 不存在 直接返回 null
            log.info("缓存未命中 返回 null");
            throw new RuntimeException("店铺信息不存在");
        }
        // 存在 把 json 序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject jsonObject = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(jsonObject, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 未过期 返回店铺信息
            log.info("缓存未过期 返回数据");
            return shop ;
        }
        // 已过期 缓存重建
        log.info("缓存数据过期 开始执行缓存重建！");
        // 获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id ;
        boolean isLock = tryLock(lockKey);
        if (isLock) {
            // 获取成功 单独开线程去缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    this.saveShop2Redis(id , RedisConstants.CACHE_SHOP_TTL);
                } finally {
                    unlock(lockKey);
                }
            }) ;
        }
        // 返回过期数据 其他的线程获取锁失败了也走这个逻辑
        return shop ;
    }

}
